<?php
require_once('class_form.php');
require_once('class_form_element_factory.php');
require_once('class_util.php');
require_once('include_config_mysql.php');

/**
 * ManagedForm
 *
 * This class utilizes the form class to automatically create forms based on the
 * layout of a mysql table. It can add, edit, delete, or copy rows in any mysql
 * table. Since form elements are loaded automatically, tools for manipulating
 * the preloaded elements are added. You can change types of form elements,
 * change elements entirely, or set various properties en masse.
 *
 * @package 
 * @author Michael Thomas
 * @copyright 2008
 * @version $Id$
 * @access public
 */
class ManagedForm extends Form
{
  // TODO: Reimplement changing of element order.

  const MODE_ADD = 0;
  const MODE_COPY = 1;
  const MODE_EDIT = 2;
  const MODE_DELETE = 3;

  const PARAM_MODE_ADD = 'a';
  const PARAM_MODE_COPY = 'c';
  const PARAM_MODE_EDIT = 'e';
  const PARAM_MODE_DELETE = 'd';

  const WARNING_DELETE_SINGLE = 'Are you sure you want to delete this record?';
  const WARNING_DELETE_MULTI = 'Are you sure you want to delete these records?';

  protected $mode;
  protected $referer;

  /* The mysql table we're using */
  protected $mysqlTable;
  protected $mysqlDatabase;
  protected $mysqlPrimaryKey;
  protected $mysqlFrom;

  /* The name of a function to call after the form successfully submits */
  protected $successHandler;

  /*
    For a single rowId, $rowId will contain the id and $rowIds will contain an
    array with the single rowId as the only element. For more than one rowId,
    $rowId will contain the first rowId, and $rowIds will contain an array of
    all rowIds. The delete operation is defined for multiple rowIds. No other
    operation is. So $rowIds is ignored unless we're deleting.
  */
  protected $rowId;
  protected $rowIds;

  protected $defaultValues;

  public $debug = false;

  public function __construct($mysqlTable, $mysqlPrimaryKey,
      $mysqlDatabase = null)
  {
    parent::__construct(self::METHOD_POST);

    $this->mysqlTable = $mysqlTable;
    $this->mysqlPrimaryKey = $mysqlPrimaryKey;
    $this->mysqlDatabase = $mysqlDatabase;

    if (isset($this->mysqlDatabase)) {
      $this->mysqlFrom = $this->mysqlDatabase . '.' . $this->mysqlTable;
    } else {
      $this->mysqlFrom = $this->mysqlTable;
    }

    $this->validateInput();
    if ($this->mode != self::MODE_ADD) {
      $this->loadDefaultValues();
    }
    $this->loadElements();
  }

  protected function validateInput()
  {
    if (isset($_GET[self::PARAM_MODE_COPY])) {

      $this->mode = self::MODE_COPY;
      $this->rowId = mysql_real_escape_string($_GET[self::PARAM_MODE_COPY]);

    } elseif (isset($_GET[self::PARAM_MODE_EDIT])) {

      $this->mode = self::MODE_EDIT;
      $this->rowId = mysql_real_escape_string($_GET[self::PARAM_MODE_EDIT]);

    } elseif (isset($_GET[self::PARAM_MODE_DELETE])) {

      $this->mode = self::MODE_DELETE;
      $rowIdParam = mysql_real_escape_string($_GET[self::PARAM_MODE_DELETE]);
      $this->rowIds = explode(' ', $rowIdParam);
      $this->rowId = $this->rowIds[0];;

    } else {

      $this->mode = self::MODE_ADD;

    }
  }

  /**
   * ManagerForm::loadElements()
   *
   * Loads information about the associated mysql table. For each column in the
   * table, creates a form element of the appropriate type.    
   *
   * @return void
   */
  protected function loadElements()
  {
    $factory = new FormElementFactory();
    $result = Util::query("SHOW COLUMNS FROM " . $this->mysqlFrom);
    while ($data = mysql_fetch_array($result, MYSQL_NUM)) {
      $element = $factory->createInstance($data);
      if (!is_null($element)) {
        $this->addElement($element);
      }
    }
  }

  public function getRowId()
  {
    return $this->rowId;
  }

  public function getMode()
  {
    return $this->mode;
  }

  public function getSuccessHandler()
  {
    return $this->successHandler;
  }

  public function setSuccessHandler($handler)
  {
    $this->successHandler = $handler;
  }

  public function setElementType($elementName, $type)
  {
    /* Right about now I wish php had better casting support */
    $newElement = $this->getElement($elementName)->convert($type);
    $this->setElement($elementName, $newElement);
  }

  public function setElement($elementName, $newElement)
  {
    foreach ($this->elements as $key => $element) {
      if ($element->getName() == $elementName) {
        $newElement->loadData($this->formData);
        $this->elements[$key] = $newElement;
        return;
      }
    }
  }

  public function setDisabled(array $elementNames, $disabled)
  {
    foreach ($elementNames as $elementName) {
      $this->getElement($elementName)->setDisabled($disabled);
    }
  }

  public function setReadOnly(array $elementNames, $readOnly)
  {
    foreach ($elementNames as $elementName) {
      $this->getElement($elementName)->setReadOnly($readOnly);
    }
  }

  public function setHidden(array $elementNames, $hidden)
  {
    foreach ($elementNames as $elementName) {
      $this->getElement($elementName)->setHidden($hidden);
    }
  }

  public function setRequired(array $elementNames, $required)
  {
    foreach ($elementNames as $elementName) {
      $this->getElement($elementName)->setRequired($required);
    }
  }

  public function setLabels(array $labels)
  {
    foreach ($labels as $elementName => $label) {
      $this->getElement($elementName)->setLabel($label);
    }
  }

  /*
    This takes an associative array of form data as input with element names
    mapped to their desired default values.
  */
  public function setDefaultValues(array $defaults)
  {
    $this->defaultValues = $defaults;
  }

  public function getDefaultValues()
  {
    return $this->defaultValues;
  }

  /*
    Unless we're adding, we need to load the database table row that we're
    working with and stick those values into $this->defaultValues. This is done
    at construction.
  */
  protected function loadDefaultValues()
  {
    $query = "SELECT *
              FROM $this->mysqlFrom
              WHERE $this->mysqlPrimaryKey = '$this->rowId'";
    $result = Util::query($query);
    if ($data = mysql_fetch_array($result, MYSQL_ASSOC)) {
      $this->defaultValues = $data;
    } else {
      throw new LogicException("Failed to load default values (the mysql " .
          "result set contained zero rows\nQuery was: '$query'");
    }
  }

  public function setValues(array $values)
  {
    foreach ($values as $elementName => $value) {
      if ($this->hasElement($elementName)) {
        $this->getElement($elementName)->setValue($value);
      }
    }
  }

  /*
    Return an array of form element names in this form. This is handy if you
    want to iterate over all the form elements from outside this class.
  */
  public function getElementNames()
  {
    $names = array();
    foreach ($this->elements as $element) {
      $names[] = $element->getName();
    }
    return $names;
  }

  public function process()
  {
    if ($this->mode != self::MODE_ADD) {
      foreach ($this->defaultValues as $elementName => $default) {
        if ($this->hasElement($elementName)) {
          $this->getElement($elementName)->setDefaultValue($default);
        }
      }
    }
    if ($this->isSubmitting()) {
      if ($this->mode != self::MODE_DELETE) {
        $this->validate();
        if ($this->isValid()) {
          $this->submit();
          $this->redirect();
        }
      } else {
        $this->submit();
        $this->redirect();
      }
    }
  }

  public function submit()
  {
    /* Get a list of all the columns in the table */
    $result = Util::query("SHOW COLUMNS FROM " . $this->mysqlFrom);
    $tableColumns = array(); 
    while ($data = mysql_fetch_array($result, MYSQL_ASSOC)) {
      $tableColumns[] = $data['Field'];  
    }

    if ($this->mode == self::MODE_EDIT) {
      $query = "UPDATE `$this->mysqlFrom` SET ";
      /* Add to our query for each column we have a form element for */
      $queryFields = array();
      foreach ($tableColumns as $columnName) {
        if ($this->hasElement($columnName)) {
          $element = $this->getElement($columnName);
          $value = Util::getMysqlSafeString($element->getValue());
          $queryFields[] = "`$columnName` = '$value'";
        }
      }
      $query .= implode(', ', $queryFields);
      $query .= " WHERE `$this->mysqlPrimaryKey` = '$this->rowId' LIMIT 1";

    } elseif ($this->mode == self::MODE_DELETE) {
      $rowIds = "'" . implode("','", $this->rowIds) . "'";
      $count = count($this->rowIds);
      $query = "DELETE FROM `$this->mysqlFrom`
                WHERE `$this->mysqlPrimaryKey` IN ($rowIds)
                LIMIT $count";
    } else {
      $query = "INSERT INTO `$this->mysqlFrom` VALUES(";
      $queryFields = array();
      foreach ($tableColumns as $columnName) {
        if ($this->hasElement($columnName)) {
          $element = $this->getElement($columnName);
          $value = Util::getMysqlSafeString($element->getValue());
          $queryFields[] = "'$value'";
        } else {
          $queryFields[] = "''";
        }
      }
      $query .= implode(', ', $queryFields) . ')';
    }
    $this->debugPrint("MAIN QUERY: $query");
    if (!$this->debug) {
      Util::query($query);
      if (isset($this->successHandler)) {
        call_user_func($this->successHandler, $this);
      }
    }
  }

  /**
   * Form::reorderElements()
   *
   * Loops through the contents of elementDisplayOrder and changes the order of
   * form elements to match. Any elements not in elementDisplayOrder get stuck 
   * at the end in their original order.
   *
   * @return void
   */
  protected function reorderElements()
  {
    // TODO: We'll want to reorder the elements on the spot
    $temp = array();
    foreach ($this->elementDisplayOrder as $elementName) {
      $temp[$elementName] = $this->elements[$elementName];
    }
    foreach ($this->elements as $name => $element) {
      if (!isset($temp[$name])) {
        $temp[$name] = $element;
      }
    }
    $this->elements = $temp;
  }

  protected function redirect()
  {
    if (isset($this->redirect)) {
      $url = $this->redirect;
    } elseif ($this->getReferer()) {
      $url = $this->getReferer();
    } else {
      $url = Util::getCurrentUrl();
    }
    header("Location: $url");
    exit();
  }

  protected function showDeleteWarning()
  {
    if (count($this->rowIds) > 1) {
      echo '<h2>' . self::WARNING_DELETE_MULTI . '</h2>';
    } else {
      echo '<h2>' . self::WARNING_DELETE_SINGLE . '</h2>';
      foreach ($this->elements AS $element) {
        echo "<strong>" . $element->getName() . "</strong>: " .
            $element->getDefaultValue() . "<br>";
      }
    }
  }

  protected function displayElements()
  {
    if ($this->mode == self::MODE_DELETE) {
      $this->showDeleteWarning();
    } else {
      parent::displayElements();
    }
  }
}

?>